<?php
/**
 * @file Implementation of the RawsConnection class.
 *
 * @author brk
 * @version 0.7
 * @copyright rambla.be, 17 October, 2007
 * @package default
 **/

/**
 * Wraps usage of php curllib for accessing Rambla's RASS web service.
 *
 * Typical usage : 
 * $rc = new RawsConnection($username, $password);
 * $rc->sendRequest("http://rass.cdn01.rambla.be/v01/users/username", "GET");
 * $code = $rc->getHttpCode();
 * Depending on the code, call other memberfunctions to get response data or errors.
 *
 * @package default
 * @author brk
 **/
class RawsConnection
{
  var $auth_user = "";
  var $auth_pwd = "";
  var $httpcode;
  var $header;
  var $xmlbody;
	var $verbose = false;
	var $postdata = "";
	var $content_type = "text/plain";
	var $slug = "";
	
	/**
	 * Class constructor.
	 *
	 * @param $user Username to be used for authentication purposes.
	 * @param $pwd Password to be used for authentication purposes.
	 * @return void
	 * @author brk
	 **/
	function __construct($user = NULL, $pwd = NULL) {
	  if ($user && $pwd) {
	    $this->setCredentials($user, $pwd);
    }
	}
	
	/**
	 * Set the username and pwd to be used for authentication purposes.
	 *
	 * @param $user Username.
	 * @param $pwd Password.
	 * @return void
	 * @author brk
	 **/
  function setCredentials($user,$pwd)
  {
    $this->auth_user = $user;
    $this->auth_pwd = $pwd;
  }
  
  /**
   * Make the underlying curl library send progress info to stdout.
   *
   * @param $verbose True or False.
   * @return void
   * @author brk
   **/
  function setVerbose($verbose) {
    $this->verbose = $verbose;
  }
  
	/**
	 * Set the data to be posted.
	 *
	 * @param $user Username.
	 * @param $pwd Password.
	 * @return void
	 * @author brk
	 **/
  function setPostdata($postdata, $mimetype = "application/x-www-form-urlencoded")
  {
    $this->postdata = $postdata;
    $this->content_type = $mimetype;
  }

  function setSlug($slug)
  {
    $this->slug = $slug;
  }
  /**
   * Http reply header in original format.
   * 
   * @return A string containing the header sections, separated by newlines. Parse this string line by line and do not have
   *  any expectations on how it will end.
   * @author brk
   **/
  function getHeader() {
    return $this->header;
  }
  
  /**
   * Get the complete body of a request. May be empty for some requests.
   *
   * @return The request body.
   * @author brk
   **/
  function getBody() {
    return $this->xmlbody;
  }
  
  /**
   * Get the numeric code of the http reply.
   *
   * @return String containing the code (eg '200')
   * @author brk
   **/
  function getHttpCode() {
    return $this->httpcode;
  }
  
  /**
   * Checks if the HTTP status code received is a success code (20x)
   *
   * @return TRUE or FALSE
   * @author brk
   **/
  function hasSucceeded()
  {
    $ret = FALSE;
    if (($this->httpcode >= 200) && ($this->httpcode < 300)) {
      $ret = TRUE;
    }
    return $ret;
  }
  
  /**
   * Sends the http request to the web service and stores the results. 
   *
   * If an input file is set for a PUT request, this function will read in the contents of the $filepath param.
   * If an output file is set for a GET request, this function will store a successful reply's body in the output file.
   *
   * @param $url Full path to the resource exposed by the web service (eg http://rass.cdn01.rambla.be/v01/users/demo).
   * @param $verb Identifier for the type of Http Request (can be GET, PUT, DELETE, HEAD, POST, OPTION - if supported by the websvc).
   * @param $filepath Path to a file that is destined to provide input (PUT request) or receive output (GET request) of the resource.
   * @param $content_type string Value for content-type header, eg "video/*" or "application/atom+xml;type=entry" (default = "text/plain").
   * @param $accept string Value for accept header, default = "application/atom+xml".
   *
   * @return void
   * @author brk
   **/
  function sendRequest($url, $verb, $filepath = null, $content_type = null, $accept = null)
  {
    $this->httpcode = NULL;
    $this->header = NULL;
    $this->xmlbody = NULL;
    $fh = NULL;
    $ch = curl_init();
    # set the content type header
    $content_type_value = $this->content_type;
    if ($content_type) {
      $content_type_value = $content_type; 
    }
    $header[] = "Content-Type: " . $content_type_value;
    # set the accept header
    $accept_value = "application/atom+xml";
    if ($accept) {
      $accept_value = $accept;
    }
    $header[] = "Accept: " . $accept_value;
    # set the slug header
    if ($this->slug) {
      $header[] = "Slug: " . $this->slug;
    }
    curl_setopt ($ch,     CURLOPT_HTTPHEADER, $header);
    curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
    curl_setopt($ch, CURLOPT_VERBOSE, $this->verbose);
   curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384);  // bigger?
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_USERPWD, "$this->auth_user:$this->auth_pwd");
    curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    curl_setopt($ch, CURLOPT_USERAGENT, 'rass client/1.0');
    curl_setopt ($ch, CURLOPT_HEADER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, True);

    $bSetReadfunc = True;
    if ($verb=='DELETE') curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'DELETE');
    elseif ($verb=='HEAD') {
      curl_setopt($ch,CURLOPT_CUSTOMREQUEST,'HEAD');
      curl_setopt($ch, CURLOPT_BINARYTRANSFER, false);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
      curl_setopt($ch, CURLOPT_NOBODY, True);
//      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
//      curl_setopt($ch, CURLOPT_FORBID_REUSE, True);
    }
    elseif ($verb=='GET')
    {
      curl_setopt($ch,CURLOPT_HTTPGET,true);
      if ($filepath != NULL)
      {
        $SetReadFunc = False;
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, False);
        $fh = fopen($filepath,'wb+');
        curl_setopt($ch,CURLOPT_FILE,$fh);
      }
    }
    elseif ($verb=='POST')
    {
      if ($filepath == NULL)
      {
        curl_setopt($ch,CURLOPT_POST,true);
        if ($this->postdata) {
          curl_setopt($ch,CURLOPT_POSTFIELDS,$this->postdata);
        }
      }
      else {
        curl_setopt($ch,CURLOPT_POST,true);
        # PROBLEM WITH CURL POST => only sends multipart/form-data (see http://owtrsnc.blogspot.com/2009/01/oss-performance-testing-video.html)
        curl_setopt($ch,CURLOPT_POSTFIELDS, array('file' => "@$filepath"));
      }
    }
    elseif ($verb=='PUT')
    {
      if ($filepath == NULL)
      {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
        curl_setopt($ch, CURLOPT_POSTFIELDS, "");
      }
      else
      {
        curl_setopt($ch,CURLOPT_PUT,true);
//        curl_setopt($ch, CURLOPT_FAILONERROR, FALSE);
//        curl_setopt($ch, CURLOPT_WRITEFUNCTION, array(&$this,'_curl_body'));
        curl_setopt($ch,CURLOPT_INFILE,fopen($filepath,'rb')); // load the file in by its resource handle
        $content_len = filesize($filepath);
        curl_setopt($ch, CURLOPT_INFILESIZE, $content_len);
      }
    }
    curl_setopt($ch, CURLOPT_HEADERFUNCTION, array(&$this,'_curl_head'));
    $buf = curl_exec($ch);
    $this->xmlbody = $buf;
    if (($buf === true) || ($buf === false)) $buf = null;
    $this->httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE );
    curl_close($ch);
    if ($fh != Null) fclose($fh);
    // WORKAROUND for GET item request : error response has been written to file
    if (($verb=='GET') && ($filepath != NULL) && ($this->httpcode >= 400)) {
      // read the xml from the file and store it in the body var
      $frc = fopen($filepath, 'r+');
      $this->xmlbody = fread($frc, filesize($filepath));
      fclose($frc);
      // delete file
      unlink($filepath);
    }
  }
  
  /**
   * Get an array of the (sub-)dirnames returned by a GET dirs request.
   *
   * @return indexed array of strings (subdirs), or FALSE in case error
   * @author brk
   **/
  function getDirNames()
  {
    $ret = array();
    if ((200 != $this->httpcode) || (!$this->xmlbody)) {
      return FALSE;
    }
    if (! $dirs_obj = simplexml_load_string($this->xmlbody) ) {
      return FALSE;
    }
    if (! $dirs_obj->dirs) {
      return FALSE;
    }
    if (! $dirs_obj->dirs->dir) {
      return $ret;
    }
    foreach($dirs_obj->dirs->dir as $dir) {
      $dir_arr = get_object_vars($dir);
      $ret[] = $dir_arr['name'];
    }
    return $ret;
  }

  /**
   * Get an array of the filenames returned by a GET items request.
   *
   * @return indexed array of strings (filenames), or FALSE in case error
   * @author brk
   **/
  function getItemNames()
  {
    $ret = array();
    if ((200 != $this->httpcode) || (!$this->xmlbody)) {
      return FALSE;
    }
    if (! $items_obj = simplexml_load_string($this->xmlbody) ) {
      return FALSE;
    }
    if (! $items_obj->items) {
      return FALSE;
    }
    if (! $items_obj->items->item) {
      return $ret;
    }
    foreach($items_obj->items->item as $item) {
      $item_arr = get_object_vars($item);
      $ret[] = $item_arr['name'];
    }
    return $ret;
  }
  
  
  /**
   * Get the message inside an RAWS error reply body.
   *
   * Parses the XML that is (expected to be) inside the body, extracts the <message> element, and returns it as a string.
   *
   * @return string containing the error-msg from the response, or an error string from the parser
   * @author brk
   **/
  function getErrorMsg() {
    // get the <request> part of the xml body in array form
    if ($req = $this->_getErrorDetails()) {
      if (array_key_exists('message', $req)) {
        // get the <message> part of <request>
        return $req['message'];
      }
      else if (array_key_exists('failure', $req)) {
        return $req['failure'];
      }
    }
    return 'Error while parsing the error response body.';
  }
  
  /**
   * Get the code inside an RAWS error reply body.
   *
   * Parses the XML that is (expected to be) inside the body, extracts the <code> element, and returns it as a string.
   *
   * @return string containing the error-code from the response, or an error string from the parser
   * @author brk
   **/
  function getErrorCode() {
    // get the <request> part of the xml body in array form
    if ($req = $this->_getErrorDetails()) {
      if (array_key_exists('code', $req)) {
        // get the <message> part of <request>
        return $req['code'];
      }
      else if (array_key_exists('failure', $req)) {
        return $req['failure'];
      }
    }
    return 'Error while parsing the error response body.';
  }

  /**
   * Returns the <request> part of the xml body in array form. If parsing fails, the function returns an array with a single failure key.
   *
   * @return array
   * @author brk
   **/
  function _getErrorDetails()
  {
    $ret = array();
    // check if return error, else return msg
    if ($noerr = $this->_isNotErrorResponse()) {
      $ret['failure'] = $noerr;
      return $ret;
    }
    // get the xml inside an object
    if (! $err_obj = simplexml_load_string($this->xmlbody) ) {
      $ret['failure'] = 'Failed parsing the error response body';
      return $ret;
    }
    // get the <request> part
    if ($req_obj = $err_obj->request) {
      // turn the request object into an array (so we can return a regular string)
      return get_object_vars($req_obj);
    }
    else {
      $ret['failure'] = 'No request element was found in the error response body';
      return $ret;
    }
  }
    
  /**
   * Simple check if the response status code is an error (+400)
   *
   * @return Empty string if response has error code. Else string indicating the response has no error status.
   * @author brk
   **/
  function _isNotErrorResponse() {
    $ret = '';
    if (!$this->httpcode) {
      $ret = 'Sorry, not a valid HTTP response.';
    }
    else if (400 > (int) $this->httpcode) {
      // no error reply
      $ret = 'Response with HTTP Status Code ' . $this->httpcode . ' is not an error response.';
    }
    else if (!$this->xmlbody) {
      $ret = 'No body found for this error response.';
    }
    return $ret;      
  }
  
  /**
   * Callback function that adds each separate part of the header info to the $header datamember.
   *
   * @param $buf Buffer containing a part of the header.
   * @return Contents of the $buf variable must be passed back to curl by this function.
   * @author brk
   **/
  function _curl_head($ch,$buf)
  {
    $ret = strlen($buf);
    $this->_append_to_this_header($buf);
		// note: HTTP 1.1 (rfc2616) says that 404 and HEAD should not have response.
		// note:  CURL will hang if we don't force close by return 0 here
		// note: http://developer.amazonwebservices.com/connect/thread.jspa?messageID=40930
		// Last Line of Headers
//    if ($this->_http_request_verb=='HEAD') return 0;
    return $ret;
  }
  
  function _append_to_this_header($buf) {
    $this->header .= $buf;
  }

// //   // func: _curl_read($ch,$buf) - $ch = Curl Handle, $buf = Data in from the Socket
//   function _curl_body($ch,$buf)
//   {
//     $ret = strlen($buf);
//     // print_r($buf);
//     $this->_append_to_this_body($buf);
//    // note: HTTP 1.1 (rfc2616) says that 404 and HEAD should not have response.
//    // note:  CURL will hang if we don't force close by return 0 here
//    // note: http://developer.amazonwebservices.com/connect/thread.jspa?messageID=40930
//    // Last Line of Headers
// //   if ($this->_http_request_verb=='HEAD') return 0;
//     return $ret;
//   }
// 
//   function _append_to_this_body($buf) {
//     $this->xmlbody .= $buf;
//   }
}
// ?>